﻿<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>大文件上传示例</title>
    <!--<script src="https://cdn.tailwindcss.com"></script> -->
    <link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
    <script src="js/tailwind.js"></script>
    <script>
        tailwind.config = {
            theme: {
                extend: {
                    colors: {
                        primary: '#3b82f6',
                        secondary: '#64748b',
                        success: '#10b981',
                        danger: '#ef4444',
                        warning: '#f59e0b',
                        info: '#06b6d4',
                        light: '#f1f5f9',
                        dark: '#1e293b',
                    },
                    fontFamily: {
                        sans: ['Inter', 'system-ui', 'sans-serif'],
                    },
                }
            }
        }
    </script>
    <style type="text/tailwindcss">
        @layer utilities {
            .content-auto {
                content-visibility: auto;
            }

            .upload-shadow {
                box-shadow: 0 10px 25px -5px rgba(59, 130, 246, 0.1), 0 8px 10px -6px rgba(59, 130, 246, 0.1);
            }

            .progress-transition {
                transition: width 0.3s ease;
            }
        }
    </style>
</head>
<body class="bg-gray-50 min-h-screen font-sans">
    <div class="container mx-auto px-4 py-12 max-w-4xl">
        <div class="text-center mb-10">
            <h1 class="text-[clamp(1.75rem,4vw,2.5rem)] font-bold text-gray-800 mb-3">大文件上传工具</h1>
            <p class="text-gray-600 max-w-2xl mx-auto">支持 GB 级大文件上传，断点续传和上传进度实时显示。适用于视频、备份文件和数据集等大型文件的传输。</p>
        </div>

        <div class="bg-white rounded-xl shadow-lg p-6 mb-8 upload-shadow transform hover:shadow-xl transition-all duration-300">
            <div class="mb-6">
                <label for="file" class="block text-sm font-medium text-gray-700 mb-2">选择文件</label>
                <div class="relative">
                    <div class="flex items-center justify-center w-full">
                        <label for="file-upload" class="flex flex-col items-center justify-center w-full h-40 border-2 border-gray-300 border-dashed rounded-lg cursor-pointer bg-gray-50 hover:bg-gray-100 transition-colors duration-300">
                            <div class="flex flex-col items-center justify-center pt-5 pb-6">
                                <i class="fa fa-cloud-upload text-4xl text-primary mb-2"></i>
                                <p class="mb-2 text-sm text-gray-500"><span class="font-semibold">点击上传文件</span> 或拖放至此处</p>
                                <p class="text-xs text-gray-500">支持最大 2GB 文件</p>
                            </div>
                            <input id="file-upload" type="file" class="hidden" />
                        </label>
                    </div>
                    <div id="selected-file" class="hidden mt-3 p-3 bg-gray-50 rounded-lg border border-gray-200">
                        <div class="flex items-center">
                            <i class="fa fa-file-text-o text-gray-400 mr-3 text-xl"></i>
                            <div class="flex-1 min-w-0">
                                <p id="file-name" class="text-sm font-medium text-gray-900 truncate"></p>
                                <p id="file-size" class="text-xs text-gray-500"></p>
                            </div>
                            <button id="remove-file" type="button" class="ml-4 text-gray-400 hover:text-gray-500 focus:outline-none">
                                <i class="fa fa-times"></i>
                            </button>
                        </div>
                    </div>
                </div>
            </div>

            <div id="upload-controls" class="hidden">
                <div class="mb-4">
                    <label class="block text-sm font-medium text-gray-700 mb-2">上传进度</label>
                    <div class="w-full bg-gray-200 rounded-full h-2.5">
                        <div id="upload-progress-bar" class="bg-primary h-2.5 rounded-full progress-transition" style="width: 0%"></div>
                    </div>
                    <div class="flex justify-between mt-1">
                        <span id="upload-percentage" class="text-xs text-gray-500">0%</span>
                        <span id="upload-speed" class="text-xs text-gray-500">0 MB/s</span>
                        <span id="upload-remaining" class="text-xs text-gray-500">剩余时间: 计算中...</span>
                    </div>
                </div>

                <div class="flex flex-wrap gap-3">
                    <button id="start-upload" type="button" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-primary hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed">
                        <i class="fa fa-upload mr-2"></i> 开始上传
                    </button>
                    <button id="pause-upload" type="button" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-warning hover:bg-warning/90 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-warning transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed hidden">
                        <i class="fa fa-pause mr-2"></i> 暂停上传
                    </button>
                    <button id="cancel-upload" type="button" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-danger hover:bg-danger/90 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-danger transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed">
                        <i class="fa fa-stop mr-2"></i> 取消上传
                    </button>
                </div>
            </div>

            <div id="upload-result" class="hidden mt-6 p-4 rounded-lg border"></div>
        </div>

        <div class="bg-white rounded-xl shadow-lg p-6 upload-shadow">
            <h2 class="text-xl font-bold text-gray-800 mb-4 flex items-center">
                <i class="fa fa-info-circle text-primary mr-2"></i> 上传须知
            </h2>
            <ul class="space-y-2 text-gray-600">
                <li class="flex items-start">
                    <i class="fa fa-check-circle text-success mt-1 mr-2"></i>
                    <span>支持断点续传，上传中断后可继续之前的进度</span>
                </li>
                <li class="flex items-start">
                    <i class="fa fa-check-circle text-success mt-1 mr-2"></i>
                    <span>大文件会自动分块上传，提高传输稳定性</span>
                </li>
                <li class="flex items-start">
                    <i class="fa fa-exclamation-triangle text-warning mt-1 mr-2"></i>
                    <span>上传过程中请不要关闭浏览器窗口</span>
                </li>
                <li class="flex items-start">
                    <i class="fa fa-exclamation-triangle text-warning mt-1 mr-2"></i>
                    <span>网络不稳定时上传速度可能会受到影响</span>
                </li>
            </ul>
        </div>
    </div>

    <footer class="bg-gray-800 text-white py-8 mt-12">
        <div class="container mx-auto px-4 max-w-4xl">
            <div class="flex flex-col md:flex-row justify-between items-center">
                <div class="mb-4 md:mb-0">
                    <p class="text-gray-400 text-sm">© 2025 大文件上传服务. 保留所有权利.</p>
                </div>
                <div class="flex space-x-6">
                    <a href="#" class="text-gray-400 hover:text-white transition-colors duration-200">
                        <i class="fa fa-question-circle"></i> 帮助中心
                    </a>
                    <a href="#" class="text-gray-400 hover:text-white transition-colors duration-200">
                        <i class="fa fa-shield"></i> 隐私政策
                    </a>
                    <a href="#" class="text-gray-400 hover:text-white transition-colors duration-200">
                        <i class="fa fa-file-text-o"></i> 服务条款
                    </a>
                </div>
            </div>
        </div>
    </footer>

    <script>
        document.addEventListener('DOMContentLoaded', function() {
            // DOM 元素
            const fileUpload = document.getElementById('file-upload');
            const selectedFile = document.getElementById('selected-file');
            const fileName = document.getElementById('file-name');
            const fileSize = document.getElementById('file-size');
            const removeFile = document.getElementById('remove-file');
            const uploadControls = document.getElementById('upload-controls');
            const startUpload = document.getElementById('start-upload');
            const pauseUpload = document.getElementById('pause-upload');
            const cancelUpload = document.getElementById('cancel-upload');
            const uploadProgressBar = document.getElementById('upload-progress-bar');
            const uploadPercentage = document.getElementById('upload-percentage');
            const uploadSpeed = document.getElementById('upload-speed');
            const uploadRemaining = document.getElementById('upload-remaining');
            const uploadResult = document.getElementById('upload-result');

            // 上传状态变量
            let selectedFileObj = null;
            let uploadController = null;
            let uploadPaused = false;
            let uploadStartTime = 0;
            let uploadedBytes = 0;
            let uploadSpeedTracker = [];
            const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB 分块大小

            // 文件选择处理
            fileUpload.addEventListener('change', function(e) {
                if (this.files.length > 0) {
                    selectedFileObj = this.files[0];
                    displaySelectedFile();
                }
            });

            // 拖放功能
            const dropArea = document.querySelector('label[for="file-upload"]');
            ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
                dropArea.addEventListener(eventName, preventDefaults, false);
            });

            function preventDefaults(e) {
                e.preventDefault();
                e.stopPropagation();
            }

            ['dragenter', 'dragover'].forEach(eventName => {
                dropArea.addEventListener(eventName, highlight, false);
            });

            ['dragleave', 'drop'].forEach(eventName => {
                dropArea.addEventListener(eventName, unhighlight, false);
            });

            function highlight() {
                dropArea.classList.add('border-primary');
                dropArea.classList.add('bg-primary/5');
            }

            function unhighlight() {
                dropArea.classList.remove('border-primary');
                dropArea.classList.remove('bg-primary/5');
            }

            dropArea.addEventListener('drop', handleDrop, false);

            function handleDrop(e) {
                const dt = e.dataTransfer;
                const files = dt.files;

                if (files.length > 0) {
                    selectedFileObj = files[0];
                    displaySelectedFile();
                }
            }

            // 显示选中的文件信息
            function displaySelectedFile() {
                if (!selectedFileObj) return;
                console.log(selectedFileObj.size)
                if (selectedFileObj.size > 1024 * 1024 * 1024  * 2) {
                    console.log('不能大于2G')
                    alert('不能大于2G')
                    return
                }
                fileName.textContent = selectedFileObj.name;
                fileSize.textContent = formatFileSize(selectedFileObj.size);

                selectedFile.classList.remove('hidden');
                uploadControls.classList.remove('hidden');
                uploadResult.classList.add('hidden');

                // 重置进度条
                resetUploadState();
            }

            // 移除选中的文件
            removeFile.addEventListener('click', function() {
                fileUpload.value = '';
                selectedFileObj = null;
                selectedFile.classList.add('hidden');
                uploadControls.classList.add('hidden');
                uploadResult.classList.add('hidden');
                resetUploadState();
            });

            // 开始上传
            startUpload.addEventListener('click', function() {
                if (!selectedFileObj) return;

                if (uploadPaused) {
                    // 恢复上传
                    uploadPaused = false;
                    startUpload.classList.add('hidden');
                    pauseUpload.classList.remove('hidden');
                    uploadChunk(uploadedBytes);
                } else {
                    // 全新上传
                    startUpload.classList.add('hidden');
                    pauseUpload.classList.remove('hidden');
                    uploadResult.classList.add('hidden');
                    uploadStartTime = Date.now();
                    uploadSpeedTracker = [];
                    uploadChunk(0);
                }
            });

            // 暂停上传
            pauseUpload.addEventListener('click', function() {
                if (uploadController) {
                    uploadPaused = true;
                    uploadController.abort();
                    startUpload.classList.remove('hidden');
                    pauseUpload.classList.add('hidden');
                    showUploadResult('上传已暂停', 'warning');
                }
            });

            // 取消上传
            cancelUpload.addEventListener('click', function() {
                if (uploadController) {
                    uploadController.abort();
                    resetUploadState();
                    showUploadResult('上传已取消', 'danger');
                }
            });

            // 上传分块
            function uploadChunk(startByte) {
                if (!selectedFileObj || uploadPaused) return;

                // 计算分块的结束位置
                const endByte = Math.min(startByte + CHUNK_SIZE, selectedFileObj.size);
                const chunk = selectedFileObj.slice(startByte, endByte);

                // 创建新的 AbortController
                uploadController = new AbortController();
                const signal = uploadController.signal;

                // 准备表单数据
                const formData = new FormData();
                formData.append('file', chunk);
                formData.append('fileName', selectedFileObj.name);
                formData.append('totalSize', selectedFileObj.size);
                formData.append('startByte', startByte);

                // 上传配置
                const uploadConfig = {
                    method: 'POST',
                    body: formData,
                    signal: signal
                };

                // 更新上传状态
                uploadedBytes = startByte;

                // 开始上传
                fetch('/api/upload', uploadConfig)
                    .then(response => {
                        if (!response.ok) {
                            throw new Error(`上传失败: ${response.statusText}`);
                        }
                        return response.json();
                    })
                    .then(data => {
                        // 更新已上传字节数
                        uploadedBytes = endByte;

                        // 计算上传进度
                        const percentComplete = Math.round((uploadedBytes / selectedFileObj.size) * 100);

                        // 更新进度条
                        updateProgress(percentComplete);

                        // 如果还有剩余数据，继续上传下一个分块
                        if (endByte < selectedFileObj.size) {
                            setTimeout(() => uploadChunk(endByte), 100);
                        } else {
                            // 上传完成
                            const uploadTime = (Date.now() - uploadStartTime) / 1000;
                            const avgSpeed = (selectedFileObj.size / uploadTime / 1024 / 1024).toFixed(2);

                            // 上传完成删除文件
                            removeFile.click();

                            resetUploadState();
                            showUploadResult(`上传完成！平均速度: ${avgSpeed} MB/s`, 'success');
                        }
                    })
                    .catch(error => {
                        if (error.name === 'AbortError') {
                            console.log('上传已中止');
                        } else {
                            console.error('上传过程中发生错误:', error);
                            resetUploadState();
                            showUploadResult(`上传失败: ${error.message}`, 'danger');
                        }
                    });

                // 监听上传进度
                const reader = new FileReader();
                reader.onprogress = function(e) {
                    if (e.lengthComputable) {
                        const chunkProgress = (e.loaded / e.total) * 100;
                        const overallProgress = Math.round(
                            (startByte / selectedFileObj.size * 100) +
                            (chunkProgress * CHUNK_SIZE / selectedFileObj.size)
                        );

                        updateProgress(overallProgress);

                        // 计算上传速度
                        const now = Date.now();
                        const elapsed = (now - uploadStartTime) / 1000;
                        const speed = (uploadedBytes + e.loaded) / elapsed / 1024 / 1024; // MB/s

                        // 记录最近10个速度样本用于平滑计算
                        uploadSpeedTracker.push(speed);
                        if (uploadSpeedTracker.length > 10) {
                            uploadSpeedTracker.shift();
                        }

                        // 计算平均速度
                        const avgSpeed = uploadSpeedTracker.reduce((a, b) => a + b, 0) / uploadSpeedTracker.length;
                        uploadSpeed.textContent = `${avgSpeed.toFixed(2)} MB/s`;

                        // 计算剩余时间
                        const remainingBytes = selectedFileObj.size - (uploadedBytes + e.loaded);
                        const remainingSeconds = remainingBytes / (avgSpeed * 1024 * 1024);

                        if (remainingSeconds < 60) {
                            uploadRemaining.textContent = `剩余时间: ${Math.ceil(remainingSeconds)} 秒`;
                        } else if (remainingSeconds < 3600) {
                            uploadRemaining.textContent = `剩余时间: ${Math.floor(remainingSeconds / 60)} 分 ${Math.ceil(remainingSeconds % 60)} 秒`;
                        } else {
                            uploadRemaining.textContent = `剩余时间: ${Math.floor(remainingSeconds / 3600)} 小时 ${Math.floor((remainingSeconds % 3600) / 60)} 分`;
                        }
                    }
                };

                reader.readAsArrayBuffer(chunk);
            }

            // 更新进度条
            function updateProgress(percent) {
                uploadProgressBar.style.width = `${percent}%`;
                uploadPercentage.textContent = `${percent}%`;
            }

            // 重置上传状态
            function resetUploadState() {
                uploadProgressBar.style.width = '0%';
                uploadPercentage.textContent = '0%';
                uploadSpeed.textContent = '0 MB/s';
                uploadRemaining.textContent = '剩余时间: 计算中...';
                uploadController = null;
                uploadPaused = false;
                uploadedBytes = 0;
                uploadStartTime = 0;
                uploadSpeedTracker = [];
                startUpload.classList.remove('hidden');
                pauseUpload.classList.add('hidden');
            }

            // 显示上传结果
            function showUploadResult(message, type) {
                uploadResult.textContent = message;

                // 设置结果样式
                uploadResult.className = 'mt-6 p-4 rounded-lg border';

                switch(type) {
                    case 'success':
                        uploadResult.classList.add('bg-success/10', 'border-success/30', 'text-success');
                        uploadResult.innerHTML = `<div class="flex items-center"><i class="fa fa-check-circle mr-2"></i> ${message}</div>`;
                        break;
                    case 'warning':
                        uploadResult.classList.add('bg-warning/10', 'border-warning/30', 'text-warning');
                        uploadResult.innerHTML = `<div class="flex items-center"><i class="fa fa-exclamation-triangle mr-2"></i> ${message}</div>`;
                        break;
                    case 'danger':
                        uploadResult.classList.add('bg-danger/10', 'border-danger/30', 'text-danger');
                        uploadResult.innerHTML = `<div class="flex items-center"><i class="fa fa-times-circle mr-2"></i> ${message}</div>`;
                        break;
                }

                uploadResult.classList.remove('hidden');
            }

            // 格式化文件大小
            function formatFileSize(bytes) {
                if (bytes === 0) return '0 Bytes';
               
                const k = 1024;
                const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
                const i = Math.floor(Math.log(bytes) / Math.log(k));
       
                return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
            }
        });
    </script>
</body>
</html>